/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package br.com.saber.certificacao.ejbs.stateless;

import br.com.saber.certificacao.ejbs.stateless.interfaces.ProvaBeanLocal;
import br.com.saber.certificacao.ejbs.stateless.interfaces.VersaoBeanLocal;
import br.com.saber.certificacao.enums.Linguagem;
import br.com.saber.certificacao.enums.TipoProva;
import br.com.saber.certificacao.enums.TipoQuestao;
import br.com.saber.certificacao.modelos.Prova;
import br.com.saber.certificacao.modelos.Questao;
import br.com.saber.certificacao.modelos.Resposta;
import br.com.saber.certificacao.modelos.Usuario;
import br.com.saber.certificacao.modelos.Versao;
import br.com.saber.certificacao.modelos.comparadores.ProvaQuestaoObjetivoCertificacaoComparator;
import br.com.saber.certificacao.modelos.comparadores.QuestaoAleatoriaComparator;
import br.com.saber.certificacao.modelos.historicos.HistoricoProvas;
import br.com.saber.certificacao.modelos.relacionamentos.ProvaQuestao;
import br.com.saber.certificacao.modelos.validadores.ProvaValidador;
import br.com.saber.certificacao.modelos.validadores.exceptions.ProvaException;
import br.com.saber.certificacao.modelos.validadores.exceptions.generica.ValidacaoExcecao;
import br.com.saber.certificacao.modelos.validadores.interfaces.ValidadorInterface;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

/**
 * Perfil 5 e 6 removido, depois que o usuário refizer a prova
 * no modo comentado ele saberá a resposta e essas questões
 * ficarão repetindo todo o tempo, ficará sem graça.
 * Perfis:
 * 1 - Questões não respondidas no idioma preferido
 * 2 - Questões não respondidas em outro idioma
 * 3 - Questões respondidas no idioma preferido
 * 4 - Quastões respondidas no idioma não preferido
 * @author gbvbahia
 */
@Stateless
public class ProvaBean implements ProvaBeanLocal {

    @EJB
    private VersaoBeanLocal versaoBean;
    @PersistenceContext(name = "jdbc/saber")
    private EntityManager manager;

    /**
     * Busca provas do usuário por status terminada ou não terminada. 
     * @param usuario que criou a prova.
     * @param status true para provas terminadas e false para provas não terminadas.
     * @return Lista de provas com base no status.
     */
    @Override
    public List<Prova> buscarProvaPorStatus(Usuario usuario, boolean status) {
        Query q = this.manager.createNamedQuery("ProvaBean.buscarProvaPorStatus");
        q.setParameter("aluno", usuario);
        q.setParameter("provaTerminada", status);
        return q.getResultList();
    }

    /**
     * Recebe uma prova que já exista no banco de dados e atualiza a mesma.
     * Deve ser utilizado para gravar as respostas marcadas e para
     * terminar a prova. Se a prova tiver acabada marque a variavel 
     * booleana da prova provaTerminada para true e o bean irá 
     * calcular a nota tirada na prova antes de salvar.
     * A prova com a nota é retornada pelo método para que você 
     * receba a nota.
     * Não pode ser atualizado a nota de uma prova comentada, já que a mesma 
     * representa uma prova direta.
     * O histórico somente é gerado para provas diretas
     * @param prova a ser atualizada
     * @see criarProvaParaUsuario para criar uma nova prova.
     * @return prova com atualizações realizadas.
     */
    @Override
    public Prova atualizarProva(Prova prova) {
        if (prova.getId() == null) {
            throw new ProvaException("provaAtualizarNova");
        }
        if (prova.isProvaTerminada() && !prova.getTipoProva().equals(TipoProva.COMENTADA)) {
            prova = calculaNotaProva(prova);
            ValidadorInterface<Prova, ProvaBeanLocal> vi = new ProvaValidador();
            vi.validar(prova, this, null);
            prova = manager.merge(prova);
            gravaProvaNoHistorico(prova, this.contaQuestoesCorretas(prova));
            prova.setTipoProva(TipoProva.COMENTADA);
            manager.merge(prova);
        } else if(!prova.getTipoProva().equals(TipoProva.COMENTADA)) {
            manager.merge(prova);
        }
        manager.flush();
        return prova;
    }

    /**
     * Cria historico da prova e grava no banco de dados 
     * para consulta futura.
     * @param prova
     * @param questoesCorretas 
     */
    @TransactionAttribute(TransactionAttributeType.MANDATORY)
    private void gravaProvaNoHistorico(Prova prova, int questoesCorretas) {
        HistoricoProvas hp = new HistoricoProvas(prova, questoesCorretas);
        manager.merge(hp);
    }

    /**
     * Cria prova com base nas informações passadas.
     * Uma nova prova se inicia como DIRETA ao terminar a prova
     * a mesma passa a ser classificada como COMENTADA.
     * Se não houver quantidades de Questões suficientes será lançada uma 
     * ValidacaoException.
     * @param tipoProva
     * @param aluno
     * @param quantidadeQuestoes
     * @param idiomaPreferido
     * @param versao
     * @see atualizarProva para atualizar uma prova no banco durante uma prova e para terminar uma prova.
     * @return A Prova criada com base nas informações passadas
     */
    @Override
    public Prova criarProvaParaUsuario(Usuario aluno, int quantidadeQuestoes, Linguagem idiomaPreferido, Versao versao, TipoProva tipoProva) {
        //Verifica se existe a quantidade de questões solicitadas cadastradas.
        validaQuantidadeQuestoes(quantidadeQuestoes, versao);
        //Cria a prova com os dados passados
        Prova provaToPersist = new Prova(versao.getCertificacao().getNome(), versao.getNome(), versao.isMother(), tipoProva, versao.getNotaParaPassar(), null, false);
        provaToPersist.setAluno(aluno);
        provaToPersist.setDataCriacao(Calendar.getInstance().getTime());
        //Realiza validação da prova criada
        ValidadorInterface<Prova, ProvaBeanLocal> vi = new ProvaValidador();
        vi.validar(provaToPersist, this, null);
        //Grava a prova no banco de dados
        this.manager.persist(provaToPersist);
        //Recupera as questões com base na regra dos perfis
        List<Questao> questoesProva = montarQuestoes(aluno, idiomaPreferido, versao, quantidadeQuestoes);
        Collections.sort(questoesProva, new QuestaoAleatoriaComparator());
        for (int i = 1; i <= questoesProva.size(); i++) {
            provaToPersist.addQuestao(questoesProva.get(i - 1), i);
        }
        //Grava as questões da prova no banco
        this.manager.merge(provaToPersist);
        this.manager.flush();
        //Retorna a prova criada
        return provaToPersist;
    }

    /**
     * Verifica se existem questões suficiente para criação da prova
     * se não houver lança uma ValidacaoException.
     * @param quantidadeSolicitada
     * @param versao 
     */
    private void validaQuantidadeQuestoes(int quantidadeSolicitada, Versao versao) {
        int questoesDisponiveis = versaoBean.contarQuestoesDaVersao(versao, null).intValue();
        if (quantidadeSolicitada > questoesDisponiveis) {
            ValidacaoExcecao ve = new ProvaException("provaQuestaoInsuficiente");
            ve.setAtributoName("Quantidade de Questões");
            throw ve;
        }
    }

    /**
     * 1 - Questões não respondidas no idioma preferido
     * 2 - Questões não respondidas em outro idioma
     * 3 - Questões respondidas no idioma preferido
     * 4 - Quastões respondidas no idioma não preferido
     * @param usuario
     * @param linguagem
     * @param versao
     * @param quantidadeQuestoes
     * @return 
     */
    private List<Questao> montarQuestoes(Usuario usuario, Linguagem linguagem, Versao versao, int quantidadeQuestoes) {
        if (versao.isMother()) {
            return montarQuestoesMother(usuario, linguagem, versao, quantidadeQuestoes);
        } else {
            return montarQuestoesVersaoSimples(usuario, linguagem, versao, quantidadeQuestoes);
        }
    }

    /**
     * Monta as questões com base na quantidade de questões solicitadas
     * respeitando todos os 4 perfils caso seja necessário.<br>
     * Somente para versão Mother
     * @param usuario
     * @param linguagem
     * @param versao
     * @param quantidadeQuestoes
     * @return Lista com questões cadastradas.
     */
    List<Questao> montarQuestoesMother(Usuario usuario, Linguagem linguagem, Versao versao, int quantidadeQuestoes) {
        Set<Versao> versoesSimples = this.versaoBean.buscarVersoesFilhasDaMother(versao);
        Integer questoesPorVersao = quantidadeQuestoes / versoesSimples.size();
        Set<Questao> toReturn = new HashSet<Questao>();
        int limit = 0;
        end:
        while (toReturn.size() < quantidadeQuestoes && limit < 500) {
            for (Versao versaoSimples : versoesSimples) {
                toReturn.addAll(montarQuestoesVersaoSimples(usuario, linguagem, versaoSimples, questoesPorVersao));
                if (toReturn.size() > quantidadeQuestoes) {
                    break end;
                }
            }
            limit++;
        }
        if (limit > 490) {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, " *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***");
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, " ProvaBean.montarQuestoesMother() limite de questões estourado: {0}", limit);
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, " *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***");
        }
        return reduzListaQuestoes(new ArrayList<Questao>(toReturn), quantidadeQuestoes);
    }

    /**
     * Monta as questões com base na quantidade de questões solicitadas
     * respeitando todos os 4 perfils caso seja necessário.<br>
     * Somente para versão simples
     * @param usuario
     * @param linguagem
     * @param versao
     * @param quantidadeQuestoes
     * @return Lista com questões cadastradas.
     */
    private List<Questao> montarQuestoesVersaoSimples(Usuario usuario, Linguagem linguagem, Versao versao, int quantidadeQuestoes) {
        List<Questao> toReturn = new ArrayList<Questao>();
        //Atender Perfil 1 - Questões não respondidas no idioma preferido
        toReturn.addAll(buscarQuatoesNaoRespondidas(usuario, linguagem, versao));
        if (toReturn.size() > quantidadeQuestoes) {
            return reduzListaQuestoes(toReturn, quantidadeQuestoes);
        }
        //Atender Perfil 2 - Questões não respondidas em outro idioma
        toReturn.addAll(buscarQuatoesNaoRespondidas(usuario, alteraLinguagem(linguagem), versao));
        if (toReturn.size() > quantidadeQuestoes) {
            return reduzListaQuestoes(toReturn, quantidadeQuestoes);
        }
        //Atender Perfil 3 - Questões respondidas erradas no idioma preferido
        toReturn.addAll(buscarQuatoesRespondidas(usuario, linguagem, versao));
        if (toReturn.size() > quantidadeQuestoes) {
            return reduzListaQuestoes(toReturn, quantidadeQuestoes);
        }
        //Atender Perfil 4 - Quastões respondidas erradas no idioma não preferido
        toReturn.addAll(buscarQuatoesRespondidas(usuario, alteraLinguagem(linguagem), versao));
        if (toReturn.size() > quantidadeQuestoes) {
            return reduzListaQuestoes(toReturn, quantidadeQuestoes);
        }
        return toReturn;
    }

    /**
     * Atende Perfil 1 2
     * 1 - Questões não respondidas no idioma preferido
     * 2 - Questões não respondidas em outro idioma
     * @param usuario
     * @param linguagem
     * @return 
     */
    private List<Questao> buscarQuatoesNaoRespondidas(Usuario usuario, Linguagem linguagem, Versao versao) {
        Query q = null;
        if (linguagem != null) {
            q = this.manager.createNamedQuery("ProvaBean.buscarQuatoesNaoRespondidas");
            q.setParameter("linguagem", linguagem);
        } else {
            q = this.manager.createNamedQuery("ProvaBean.buscarQuatoesNaoRespondidasSemLinguagem");
        }
        q.setParameter("statusQuestao", true);
        q.setParameter("usuario", usuario);
        q.setParameter("versao", versao);
        return q.getResultList();
    }

    /**
     * Atende Perfil 3 e 4
     * 3 - Questões respondidas no idioma preferido
     * 4 - Quastões respondidas no idioma não preferido
     * @param usuario
     * @param linguagem
     * @param corretamente
     * @return 
     */
    private List<Questao> buscarQuatoesRespondidas(Usuario usuario, Linguagem linguagem, Versao versao) {
        Query q = null;
        if (linguagem != null) {
            q = this.manager.createNamedQuery("ProvaBean.buscarQuatoesRespondidas");
            q.setParameter("linguagem", linguagem);
        } else {
            q = this.manager.createNamedQuery("ProvaBean.buscarQuatoesRespondidasSemLinguagem");
        }
        q.setParameter("statusQuestao", true);
        q.setParameter("usuario", usuario);
        q.setParameter("versao", versao);
        return q.getResultList();
    }

    /**
     * Altera a linguajem para Inglês se for Português e
     * para Português se fo Inglês
     * @param linguagem
     * @return 
     */
    private Linguagem alteraLinguagem(Linguagem linguagem) {
        if (linguagem.equals(Linguagem.PORTUGUES)) {
            return Linguagem.INGLES;
        } else {
            return Linguagem.PORTUGUES;
        }
    }

    /**
     * Reduz a quantidade de questões dentro da lista, retornando a quantidade
     * de questões solicitadas pelo aluno.
     * @param questoes
     * @param quantidade
     * @return 
     */
    private List<Questao> reduzListaQuestoes(List<Questao> questoes, int quantidade) {
        Random random = new Random();
        Collections.sort(questoes, new QuestaoAleatoriaComparator());
        int questoesTotal = questoes.size();
        int questoesToRemove = questoesTotal - quantidade;
        Set<Questao> toRemove = new TreeSet<Questao>();
        for (int i = 0; i < questoesToRemove; i++) {
            int limite = 0;
            while (!toRemove.add(questoes.get(random.nextInt(questoesTotal)))) {
                if (++limite > 1000) {//Meio feio, mas garante sair de um eventual loop infinito
                    Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "ProvaBean.reduzListaQuestoes() estorou o limite! Questoes: {0} Quantidade: {1}", new Object[]{questoesTotal, quantidade});
                    break;
                }
            }
        }
        questoes.removeAll(toRemove);
        return questoes;
    }

    /**
     * Verifica se as respostas marcadas estão corretas
     * @param provaQuestao objeto que representa uma questão da prova.
     * @return verdadeiro se resposta correta, falso se estiver errada.
     */
    @Override
    public boolean isRespostaCorreta(ProvaQuestao provaQuestao) {
        if (provaQuestao.getQuestao().getTipoQuestao().equals(TipoQuestao.UNICA_ESCOLHA)) {
            for (Resposta res : provaQuestao.getRespostasMarcadasNaProva()) {
                if (res == null) {
                    //Não marcou resposta
                    return false;
                } else if (res.isCorreta()) {
                    return true;
                } else {
                    return false;
                }
            }
        } else {
            int respostasNecessairas = 0;
            int respostasMarcadasCorretamente = 0;
            for (Resposta res : provaQuestao.getRespostasMarcadasNaProva()) {
                if (res == null) {
                    //Não marcou resposta
                    return false;
                }
                if (res.isCorreta()) {
                    respostasMarcadasCorretamente++;
                } else {
                    return false;
                }
            }
            for (Resposta res : provaQuestao.getQuestao().getRespostas()) {
                if (res.isCorreta()) {
                    respostasNecessairas++;
                }
            }
            if (respostasMarcadasCorretamente == respostasNecessairas) {
                return true;
            } else {
                return false;
            }
        }
        return false;
    }

    /**
     * Calcula a nota tirada na prova
     * @param prova
     * @return Prova com a nota tirada calculada.
     */
    private Prova calculaNotaProva(Prova prova) {
        double questaoCerta = 0;
        double quantidadeQuestoes = 0;
        proximaQuestao:
        for (ProvaQuestao pq : prova.getQuestoesDestaProva()) {
            if (isRespostaCorreta(pq)) {
                questaoCerta++;
            }
            quantidadeQuestoes++;
        }
        prova.setNotaTirada((questaoCerta / quantidadeQuestoes) * 100);
        return prova;
    }

    /**
     * Contabiliza a quantidade de questões corretas da prova
     * @param prova
     * @return quantidade de questões corretas.
     */
    private int contaQuestoesCorretas(Prova prova) {
        int toReturn = 0;
        for (ProvaQuestao pq : prova.getQuestoesDestaProva()) {
            if (isRespostaCorreta(pq)) {
                toReturn++;
            }
        }
        return toReturn;
    }

    @Override
    public Map<String, Number> obterObjetivosProQuantidadeDeErros(Prova provaTerminada) {
        if (!provaTerminada.isProvaTerminada()) {
            return null;//Se prova não tiver terminada retorna null
        }
        Integer totalErros = 0;
        Map<String, Integer> totalErrosMap = new LinkedHashMap<String, Integer>();
        List<ProvaQuestao> provaQuestoes = new ArrayList<ProvaQuestao>(provaTerminada.getQuestoesDestaProva());
        Collections.sort(provaQuestoes, new ProvaQuestaoObjetivoCertificacaoComparator());
        //Contabiliza o total de erros em totalErros
        //Contabiliza os erros por objetivo em totalErrosMap
        for (ProvaQuestao pq : provaQuestoes) {
            if (!this.isRespostaCorreta(pq)) {
                totalErros++;
                if (!totalErrosMap.containsKey(pq.getQuestao().getObjetivoCertificacao())) {
                    totalErrosMap.put(pq.getQuestao().getObjetivoCertificacao(), 1);
                } else {
                    totalErrosMap.put(pq.getQuestao().getObjetivoCertificacao(), (totalErrosMap.get(pq.getQuestao().getObjetivoCertificacao()) + 1));
                }
            }
        }
        //Insere dentro do map o % de erros sobre o total de questoes
        Map<String, Number> toReturn = new LinkedHashMap<String, Number>();
        for (String key : totalErrosMap.keySet()) {
            toReturn.put(key.replace("\n", "").replace("\r", ""), totalErrosMap.get(key).doubleValue() / totalErros.doubleValue());
        }
        return toReturn;
    }

    @Override
    public Map<String, Integer[]> obterObjetivosIncorretosComTotalQuestoesPorObjetivos(Prova provaTerminada) {
        if (!provaTerminada.isProvaTerminada()) {
            return null;//Se prova não tiver terminada retorna null
        }
        List<ProvaQuestao> provaQuestoes = new ArrayList<ProvaQuestao>(provaTerminada.getQuestoesDestaProva());
        Collections.sort(provaQuestoes, new ProvaQuestaoObjetivoCertificacaoComparator());
        Map<String, Integer[]> toReturn = new LinkedHashMap<String, Integer[]>();
        for (ProvaQuestao pq : provaQuestoes) {
            String key = pq.getQuestao().getObjetivoCertificacao().replace("\n", "").replace("\r", "");
            if (toReturn.containsKey(key)) {
                Integer[] temp = toReturn.get(key);
                temp[1] = temp[1]+1;
                if (!this.isRespostaCorreta(pq)) {
                    temp[0] = temp[0]+1;
                }
                toReturn.put(key, temp);
            } else {
                if (this.isRespostaCorreta(pq)) {
                    toReturn.put(key, new Integer[]{0, 1});
                } else {
                    toReturn.put(key, new Integer[]{1, 1});
                }
            }
        }

        return toReturn;
    }
}
